﻿
using Application = System.Windows.Forms.Application;
using IWin32Window = System.Windows.Forms.IWin32Window;
using MethodInvoker = System.Windows.Forms.MethodInvoker;
using Size = System.Drawing.Size;
using Timer = System.Timers.Timer;

namespace Krypton.Toolkit.Suite.Extended.Software.Updater
{
    /// <summary>
    ///     Main class that lets you auto update applications by setting some static fields and executing its Start method.
    /// </summary>
    public static class AutoUpdater
    {
        /// <summary>
        ///     A delegate type to handle how to exit the application after update is downloaded.
        /// </summary>
        public delegate void ApplicationExitEventHandler();

        /// <summary>
        ///     A delegate type for hooking up update notifications.
        /// </summary>
        /// <param name="args">
        ///     An object containing all the parameters received from AppCast XML file. If there will be an error
        ///     while looking for the XML file then this object will be null.
        /// </param>
        public delegate void CheckForUpdateEventHandler(UpdateInfoEventArgs args);

        /// <summary>
        ///     A delegate type for hooking up parsing logic.
        /// </summary>
        /// <param name="args">An object containing the AppCast file received from server.</param>
        public delegate void ParseUpdateInfoHandler(ParseUpdateInfoEventArgs args);

        private static bool _isWinFormsApplication;

        private static IWin32Window _owner;

        private static Timer _remindLaterTimer;

        internal static Uri BaseUri;

        internal static bool Running;

        /// <summary>
        ///     URL of the xml file that contains information about latest version of the application.
        /// </summary>
        public static string AppCastURL;

        /// <summary>
        ///     Set the Application Title shown in Update dialog. Although AutoUpdater.NET will get it automatically, you can set
        ///     this property if you like to give custom Title.
        /// </summary>
        public static string AppTitle;

        /// <summary>
        ///     Set Basic Authentication credentials to navigate to the change log URL.
        /// </summary>
        public static IAuthentication BasicAuthChangeLog;

        /// <summary>
        ///     Set Basic Authentication credentials required to download the file.
        /// </summary>
        public static IAuthentication BasicAuthDownload;

        /// <summary>
        ///     Set Basic Authentication credentials required to download the XML file.
        /// </summary>
        public static IAuthentication BasicAuthXML;

        /// <summary>
        ///     Set this to true if you want to clear application directory before extracting update.
        /// </summary>
        public static bool ClearAppDirectory = false;

        /// <summary>
        ///     Set it to folder path where you want to download the update file. If not provided then it defaults to Temp folder.
        /// </summary>
        public static string DownloadPath;

        /// <summary>
        ///     If you are using a zip file as an update file, then you can set this value to a new executable path relative to the
        ///     installation directory.
        /// </summary>
        public static string? ExecutablePath;

        /// <summary>
        ///     Login/password/domain for FTP-request
        /// </summary>
        public static NetworkCredential FtpCredentials;

        /// <summary>
        ///     Set the User-Agent string to be used for HTTP web requests.
        /// </summary>
        public static string HttpUserAgent;

        /// <summary>Set this to change the icon shown on updater dialog.</summary>
        public static Bitmap? Icon;

        /// <summary>
        ///     If you are using a zip file as an update file then you can set this value to path where your app is installed. This
        ///     is only necessary when your installation directory differs from your executable path.
        /// </summary>
        public static string? InstallationPath;

        /// <summary>
        ///     You can set this field to your current version if you don't want to determine the version from the assembly.
        /// </summary>
        public static Version InstalledVersion;

        /// <summary>
        ///     If this is true users see dialog where they can set remind later interval otherwise it will take the interval from
        ///     RemindLaterAt and RemindLaterTimeSpan fields.
        /// </summary>
        public static bool LetUserSelectRemindLater = true;

        /// <summary>
        ///     Set this to true if you want to ignore previously assigned Remind Later and Skip settings. It will also hide Remind
        ///     Later and Skip buttons.
        /// </summary>
        public static bool Mandatory;

        /// <summary>
        ///     Opens the download URL in default browser if true. Very useful if you have portable application.
        /// </summary>
        public static bool OpenDownloadPage;

        /// <summary>
        ///     Set this to an instance implementing the IPersistenceProvider interface for using a data storage method different
        ///     from the default Windows Registry based one.
        /// </summary>
        public static IPersistenceProvider PersistenceProvider;

        /// <summary>
        ///     Set Proxy server to use for all the web requests in AutoUpdater.NET.
        /// </summary>
        public static IWebProxy Proxy;

        /// <summary>
        ///     Remind Later interval after user should be reminded of update.
        /// </summary>
        public static int RemindLaterAt = 2;

        /// <summary>
        ///     Set if RemindLaterAt interval should be in Minutes, Hours or Days.
        /// </summary>
        public static RemindLaterFormat RemindLaterTimeSpan = RemindLaterFormat.Days;

        /// <summary>
        ///     AutoUpdater.NET will report errors if this is true.
        /// </summary>
        public static bool ReportErrors = false;

        /// <summary>
        ///     Set this to false if your application doesn't need administrator privileges to replace the old version.
        /// </summary>
        public static bool RunUpdateAsAdmin = true;

        /// <summary>
        ///     If this is true users can see the Remind Later button.
        /// </summary>
        public static bool ShowRemindLaterButton = true;

        /// <summary>
        ///     If this is true users can see the skip button.
        /// </summary>
        public static bool ShowSkipButton = true;

        /// <summary>
        ///     Set this to true if you want to run update check synchronously.
        /// </summary>
        public static bool Synchronous = false;

        /// <summary>
        ///     Modify TopMost property of all dialogs.
        /// </summary>
        public static bool TopMost = false;

        /// <summary>
        ///     Set this if you want the default update form to have a different size.
        /// </summary>
        public static Size? UpdateFormSize = null;

        /// <summary>
        ///     Set this to any of the available modes to change behaviour of the Mandatory flag.
        /// </summary>
        public static Mode UpdateMode;

        /// <summary>
        ///     An event that developers can use to exit the application gracefully.
        /// </summary>
        public static event ApplicationExitEventHandler ApplicationExitEvent;

        /// <summary>
        ///     An event that clients can use to be notified whenever the update is checked.
        /// </summary>
        public static event CheckForUpdateEventHandler CheckForUpdateEvent;

        /// <summary>
        ///     An event that clients can use to be notified whenever the AppCast file needs parsing.
        /// </summary>
        public static event ParseUpdateInfoHandler ParseUpdateInfoEvent;

        /// <summary>
        ///     Set the owner for all dialogs.
        /// </summary>
        /// <param name="obj">WPF Window or Windows Form object to be used as owner for all dialogs.</param>
        public static void SetOwner(object obj)
        {
            _owner = obj switch
            {
                Form form => form,
                Window window => new Wpf32Window(window),
                _ => _owner
            };
        }

        /// <summary>
        ///     Start checking for new version of application and display a dialog to the user if update is available.
        /// </summary>
        /// <param name="myAssembly">Assembly to use for version checking.</param>
        public static void Start(Assembly myAssembly = null)
        {
            Start(AppCastURL, myAssembly);
        }

        /// <summary>
        ///     Start checking for new version of application via FTP and display a dialog to the user if update is available.
        /// </summary>
        /// <param name="appCast">FTP URL of the xml file that contains information about latest version of the application.</param>
        /// <param name="ftpCredentials">Credentials required to connect to FTP server.</param>
        /// <param name="myAssembly">Assembly to use for version checking.</param>
        public static void Start(string appCast, NetworkCredential ftpCredentials, Assembly myAssembly = null)
        {
            FtpCredentials = ftpCredentials;
            Start(appCast, myAssembly);
        }

        /// <summary>
        ///     Start checking for new version of application and display a dialog to the user if update is available.
        /// </summary>
        /// <param name="appCast">URL of the xml file that contains information about latest version of the application.</param>
        /// <param name="myAssembly">Assembly to use for version checking.</param>
        public static void Start(string appCast, Assembly myAssembly = null)
        {
            try
            {
                ServicePointManager.SecurityProtocol |= (SecurityProtocolType)192 |
                                                        (SecurityProtocolType)768 | (SecurityProtocolType)3072;
            }
            catch (NotSupportedException)
            {
            }

            if (Mandatory && _remindLaterTimer != null)
            {
                _remindLaterTimer.Stop();
                _remindLaterTimer.Close();
                _remindLaterTimer = null;
            }

            if (Running || _remindLaterTimer != null)
            {
                return;
            }

            Running = true;

            AppCastURL = appCast;

            _isWinFormsApplication = Application.MessageLoop;

            if (!_isWinFormsApplication)
            {
                Application.EnableVisualStyles();
            }

            Assembly assembly = myAssembly ?? Assembly.GetEntryAssembly();

            if (Synchronous)
            {
                try
                {
                    object result = CheckUpdate(assembly);

                    if (StartUpdate(result))
                    {
                        return;
                    }

                    Running = false;
                }
                catch (Exception exception)
                {
                    ShowError(exception);
                }
            }
            else
            {
                using var backgroundWorker = new BackgroundWorker();

                backgroundWorker.DoWork += (_, args) =>
                {
                    var mainAssembly = args.Argument as Assembly;

                    args.Result = CheckUpdate(mainAssembly);
                };

                backgroundWorker.RunWorkerCompleted += (_, args) =>
                {
                    if (args.Error != null)
                    {
                        ShowError(args.Error);
                    }
                    else
                    {
                        if (!args.Cancelled && StartUpdate(args.Result))
                        {
                            return;
                        }

                        Running = false;
                    }
                };

                backgroundWorker.RunWorkerAsync(assembly);
            }
        }

        private static object CheckUpdate(Assembly mainAssembly)
        {
            var companyAttribute =
                (AssemblyCompanyAttribute)GetAttribute(mainAssembly, typeof(AssemblyCompanyAttribute));
            string appCompany = companyAttribute != null ? companyAttribute.Company : "";

            if (string.IsNullOrEmpty(AppTitle))
            {
                var titleAttribute =
                    (AssemblyTitleAttribute)GetAttribute(mainAssembly, typeof(AssemblyTitleAttribute));
                AppTitle = titleAttribute != null ? titleAttribute.Title : mainAssembly.GetName().Name;
            }

            string registryLocation = !string.IsNullOrEmpty(appCompany)
                ? $@"Software\{appCompany}\{AppTitle}\AutoUpdater"
                : $@"Software\{AppTitle}\AutoUpdater";

            PersistenceProvider ??= new RegistryPersistenceProvider(registryLocation);

            BaseUri = new Uri(AppCastURL);

            UpdateInfoEventArgs args;
            using (MyWebClient client = GetWebClient(BaseUri, BasicAuthXML))
            {
                string xml = client.DownloadString(BaseUri);

                if (ParseUpdateInfoEvent == null)
                {
                    var xmlSerializer = new XmlSerializer(typeof(UpdateInfoEventArgs));
                    var xmlTextReader = new XmlTextReader(new StringReader(xml)) { XmlResolver = null };
                    args = (UpdateInfoEventArgs)xmlSerializer.Deserialize(xmlTextReader);
                }
                else
                {
                    var parseArgs = new ParseUpdateInfoEventArgs(xml);
                    ParseUpdateInfoEvent(parseArgs);
                    args = parseArgs.UpdateInfo;
                }
            }

            if (string.IsNullOrEmpty(args?.CurrentVersion) || string.IsNullOrEmpty(args.DownloadURL))
            {
                throw new MissingFieldException();
            }

            args.InstalledVersion = InstalledVersion ?? mainAssembly.GetName().Version;
            args.IsUpdateAvailable = new Version(args.CurrentVersion) > args.InstalledVersion;

            if (!Mandatory)
            {
                if (string.IsNullOrEmpty(args.Mandatory.MinimumVersion) ||
                    args.InstalledVersion < new Version(args.Mandatory.MinimumVersion))
                {
                    Mandatory = args.Mandatory.Value;
                    UpdateMode = args.Mandatory.UpdateMode;
                }
            }

            if (Mandatory)
            {
                ShowRemindLaterButton = false;
                ShowSkipButton = false;
            }
            else
            {
                // Read the persisted state from the persistence provider.
                // This method makes the persistence handling independent from the storage method.
                Version? skippedVersion = PersistenceProvider.GetSkippedVersion();
                if (skippedVersion != null)
                {
                    var currentVersion = new Version(args.CurrentVersion);
                    if (currentVersion <= skippedVersion)
                    {
                        return null;
                    }

                    if (currentVersion > skippedVersion)
                    {
                        // Update the persisted state. Its no longer makes sense to have this flag set as we are working on a newer application version.
                        PersistenceProvider.SetSkippedVersion(null);
                    }
                }

                DateTime? remindLaterAt = PersistenceProvider.GetRemindLater();
                if (remindLaterAt == null)
                {
                    return args;
                }

                int compareResult = DateTime.Compare(DateTime.Now, remindLaterAt.Value);

                if (compareResult < 0)
                {
                    return remindLaterAt.Value;
                }
            }

            return args;
        }

        private static bool StartUpdate(object result)
        {
            if (result is DateTime time)
            {
                SetTimer(time);
            }
            else
            {
                if (result is not UpdateInfoEventArgs args)
                {
                    return false;
                }

                if (CheckForUpdateEvent != null)
                {
                    CheckForUpdateEvent(args);
                }
                else
                {
                    if (args.IsUpdateAvailable)
                    {
                        if (Mandatory && UpdateMode == Mode.ForcedDownload)
                        {
                            DownloadUpdate(args);
                            Exit();
                        }
                        else
                        {
                            if (Thread.CurrentThread.GetApartmentState().Equals(ApartmentState.STA))
                            {
                                ShowUpdateForm(args);
                            }
                            else
                            {
                                var thread = new Thread(new ThreadStart(delegate { ShowUpdateForm(args); }));
                                thread.CurrentCulture =
                                    thread.CurrentUICulture = CultureInfo.CurrentCulture;
                                thread.SetApartmentState(ApartmentState.STA);
                                thread.Start();
                                thread.Join();
                            }
                        }

                        return true;
                    }

                    if (ReportErrors)
                    {
                        KryptonMessageBox.Show(_owner,
                            AutoUpdaterLanguageManager.UpdaterStrings.UpdateUnavailableMessage,
                            AutoUpdaterLanguageManager.UpdaterStrings.UpdateUnavailableCaption,
                            KryptonMessageBoxButtons.OK, KryptonMessageBoxIcon.Information);
                    }
                }
            }

            return false;
        }

        private static void ShowError(Exception exception)
        {
            if (CheckForUpdateEvent != null)
            {
                CheckForUpdateEvent(new UpdateInfoEventArgs { Error = exception });
            }
            else
            {
                if (ReportErrors)
                {
                    if (exception is WebException)
                    {
                        KryptonMessageBox.Show(_owner,
                            AutoUpdaterLanguageManager.UpdaterStrings.UpdateCheckFailedMessage,
                            AutoUpdaterLanguageManager.UpdaterStrings.UpdateCheckFailedCaption,
                            KryptonMessageBoxButtons.OK, KryptonMessageBoxIcon.Error);
                    }
                    else
                    {
                        KryptonMessageBox.Show(_owner,
                            exception.Message,
                            exception.GetType().ToString(),
                            KryptonMessageBoxButtons.OK, KryptonMessageBoxIcon.Error);
                    }
                }
            }

            Running = false;
        }

        /// <summary>
        ///     Detects and exits all instances of running assembly, including current.
        /// </summary>
        internal static void Exit()
        {
            var currentProcess = Process.GetCurrentProcess();
            foreach (Process process in Process.GetProcessesByName(currentProcess.ProcessName))
            {
                string processPath;
                try
                {
                    processPath = process.MainModule?.FileName;
                }
                catch (Win32Exception)
                {
                    // Current process should be same as processes created by other instances of the application so it should be able to access modules of other instances. 
                    // This means this is not the process we are looking for so we can safely skip this.
                    continue;
                }

                // Get all instances of assembly except current
                if (process.Id == currentProcess.Id || currentProcess.MainModule?.FileName != processPath)
                {
                    continue;
                }

                if (process.CloseMainWindow())
                {
                    process.WaitForExit((int)TimeSpan.FromSeconds(10)
                        .TotalMilliseconds); // Give some time to process message
                }

                if (!process.HasExited)
                {
                    process.Kill(); //TODO: Show UI message asking user to close program himself instead of silently killing it
                }
            }

            if (ApplicationExitEvent != null)
            {
                ApplicationExitEvent();
            }
            else
            {
                if (_isWinFormsApplication)
                {
                    MethodInvoker methodInvoker = Application.Exit;
                    methodInvoker.Invoke();
                }
                else if (System.Windows.Application.Current != null)
                {
                    System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
                        System.Windows.Application.Current.Shutdown()));
                }
                else
                {
                    Environment.Exit(0);
                }
            }
        }

        private static Attribute GetAttribute(Assembly assembly, Type attributeType)
        {
            object[] attributes = assembly.GetCustomAttributes(attributeType, false);
            if (attributes.Length == 0)
            {
                return null;
            }

            return (Attribute)attributes[0];
        }

        internal static string GetUserAgent()
        {
            return string.IsNullOrEmpty(HttpUserAgent) ? "AutoUpdater.NET" : HttpUserAgent;
        }

        internal static void SetTimer(DateTime remindLater)
        {
            TimeSpan timeSpan = remindLater - DateTime.Now;

            SynchronizationContext context = SynchronizationContext.Current;

            _remindLaterTimer = new Timer
            {
                Interval = Math.Max(1, timeSpan.TotalMilliseconds),
                AutoReset = false
            };

            _remindLaterTimer.Elapsed += delegate
            {
                _remindLaterTimer = null;
                if (context != null)
                {
                    try
                    {
                        context.Send(_ => Start(), null);
                    }
                    catch (InvalidAsynchronousStateException)
                    {
                        Start();
                    }
                }
                else
                {
                    Start();
                }
            };

            _remindLaterTimer.Start();
        }

        /// <summary>
        ///     Opens the Download window that download the update and execute the installer when download completes.
        /// </summary>
        public static bool DownloadUpdate(UpdateInfoEventArgs args)
        {
            using var downloadDialog = new DownloadUpdateDialog(args);

            try
            {
                return downloadDialog.ShowDialog(_owner).Equals(DialogResult.OK);
            }
            catch (TargetInvocationException)
            {
                // ignored
            }

            return false;
        }

        /// <summary>
        ///     Shows standard update dialog.
        /// </summary>
        public static void ShowUpdateForm(UpdateInfoEventArgs args)
        {
            using var updateForm = new UpdateDialog(args);

            if (UpdateFormSize.HasValue)
            {
                updateForm.Size = UpdateFormSize.Value;
            }

            if (updateForm.ShowDialog(_owner).Equals(DialogResult.OK))
            {
                Exit();
            }
        }

        internal static MyWebClient GetWebClient(Uri uri, IAuthentication basicAuthentication)
        {
            var webClient = new MyWebClient
            {
                CachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore)
            };

            if (Proxy != null)
            {
                webClient.Proxy = Proxy;
            }

            if (uri.Scheme.Equals(Uri.UriSchemeFtp))
            {
                webClient.Credentials = FtpCredentials;
            }
            else
            {
                basicAuthentication?.Apply(ref webClient);

                webClient.Headers[HttpRequestHeader.UserAgent] = HttpUserAgent;
            }

            return webClient;
        }
    }
}